/*
 * UAE - The Un*x Amiga Emulator
 *
 * Library of functions to make emulated filesystem as independent as
 * possible of the host filesystem's capabilities.
 *
 * Copyright 1999 Bernd Schmidt
 */

#include "sysconfig.h"
#include "fsdb.h"
#include "fsusage.h"
#include "filesys.h"
#include "hardfile.h"
#include "memory.h"

/* The on-disk format is as follows:
 * Offset 0, 1 byte, valid
 * Offset 1, 4 bytes, mode
 * Offset 5, 257 bytes, aname
 * Offset 263, 257 bytes, nname
 * Offset 519, 81 bytes, comment
 */

#if FSDB_DEBUG_DEF
    #define TRACE_FSDB(x) do { Logger::Write x; } while (0)
#else
    #define TRACE_FSDB(x)
#endif

TCHAR* nname_begin(TCHAR* nname)
{
    TCHAR* p = _tcsrchr(nname, FSDB_DIR_SEPARATOR);
    if (p)
        return p + 1;
    return nname;
}

#ifndef _WIN32
/* Find the name REL in directory DIRNAME.  If we find a file that
 * has exactly the same name, return REL.  If we find a file that
 * has the same name when compared case-insensitively, return a
 * malloced string that contains the name we found.  If no file
 * exists that compares equal to REL, return 0.  */
TCHAR* fsdb_search_dir(const TCHAR* dirname, TCHAR* rel)
{
    TCHAR* p = 0;
    int de;
    void* dir;
    TCHAR fn[MAX_DPATH];

    dir = my_opendir(dirname);
    /* This really shouldn't happen...  */
    if (!dir)
        return 0;

    while (p == 0 && (de = my_readdir(dir, fn)) != 0)
    {
        if (strcmp(fn, rel) == 0)
            p = rel;
        else if (_tcsicmp(fn, rel) == 0)
            p = _tcsdup(fn);
    }
    my_closedir(dir);
    return p;
}
#endif

static FILE* get_fsdb(a_inode* dir, const TCHAR* mode)
{
    TCHAR* n;
    FILE* f;

    n = build_nname(dir->nname, FSDB_FILE);
    f = _tfopen(n, mode);
    free(n);
    return f;
}

static void kill_fsdb(a_inode* dir)
{
    TCHAR* n = build_nname(dir->nname, FSDB_FILE);
    _wunlink(n);
    free(n);
}

static void fsdb_fixup(FILE* f, TCHAR* buf, int size, a_inode* base)
{
    TCHAR* nname;
    int ret;

    if (buf[0] == 0)
        return;
    nname = build_nname(base->nname, buf + 5 + 257);
    ret = fsdb_exists(nname);
    if (ret)
    {
        free(nname);
        return;
    }
    TRACE_FSDB((L"uaefsdb '%s' deleted\n", nname));
    /* someone deleted this file/dir outside of emulation.. */
    buf[0] = 0;
    free(nname);
}

/* Prune the db file the first time this directory is opened in a session.  */
void fsdb_clean_dir(a_inode* dir)
{
    TCHAR buf[1 + 4 + 257 + 257 + 81];
    TCHAR* n;
    FILE* f;
    off_t pos1 = 0, pos2;

    n = build_nname(dir->nname, FSDB_FILE);
    f = _tfopen(n, L"r+b");
    if (f == 0)
    {
        free(n);
        return;
    }
    for (;;)
    {
        pos2 = ftell(f);
        if (fread(buf, 1, sizeof buf, f) < sizeof buf)
            break;
        fsdb_fixup(f, buf, sizeof buf, dir);
        if (buf[0] == 0)
            continue;
        if (pos1 != pos2)
        {
            fseek(f, pos1, SEEK_SET);
            fwrite(buf, 1, sizeof buf, f);
            fseek(f, pos2 + sizeof buf, SEEK_SET);
        }
        pos1 += sizeof buf;
    }
    fclose(f);
    my_truncate(n, pos1);
    free(n);
}

static a_inode* aino_from_buf(a_inode* base, byte* buf, long off)
{
    uint mode;
    a_inode* aino = xcalloc(a_inode, 1);
    TCHAR* s;

    mode = do_get_mem_long((uint*)(buf + 1));
    buf += 5;
    aino->aname = Unicode::au((char*)buf);
    buf += 257;
    s = Unicode::au((char*)buf);
    aino->nname = build_nname(base->nname, s);
    free(s);
    buf += 257;
    aino->comment = *buf != '\0' ? Unicode::au((char*)buf) : 0;
    fsdb_fill_file_attrs(base, aino);
    aino->amigaos_mode = mode;
    aino->has_dbentry = 1;
    aino->dirty = 0;
    aino->db_offset = off;
    return aino;
}

a_inode* fsdb_lookup_aino_aname(a_inode* base, const TCHAR* aname)
{
    FILE* f;

    f = get_fsdb(base, L"r+b");
    if (f == 0)
    {
        if (g_curr_conf.filesys_custom_uaefsdb && (base->volflags & MYVOLUMEINFO_STREAMS))
            return custom_fsdb_lookup_aino_aname(base, aname);
        return 0;
    }
    for (;;)
    {
        byte buf[1 + 4 + 257 + 257 + 81];
        TCHAR* s;
        if (fread(buf, 1, sizeof buf, f) < sizeof buf)
            break;
        s = Unicode::au((char*)buf + 5);
        if (buf[0] != 0 && Unicode::same_aname(s, aname))
        {
            long pos = ftell(f) - sizeof buf;
            fclose(f);
            free(s);
            return aino_from_buf(base, buf, pos);
        }
        free(s);
    }
    fclose(f);
    return 0;
}

a_inode* fsdb_lookup_aino_nname(a_inode* base, const TCHAR* nname)
{
    FILE* f;
    char* s;

    f = get_fsdb(base, L"r+b");
    if (f == 0)
    {
        if (g_curr_conf.filesys_custom_uaefsdb && (base->volflags & MYVOLUMEINFO_STREAMS))
            return custom_fsdb_lookup_aino_nname(base, nname);
        return 0;
    }
    s = Unicode::ua(nname);
    for (;;)
    {
        byte buf[1 + 4 + 257 + 257 + 81];
        if (fread(buf, 1, sizeof buf, f) < sizeof buf)
            break;
        if (buf[0] != 0 && strcmp((char*)buf + 5 + 257, s) == 0)
        {
            long pos = ftell(f) - sizeof buf;
            fclose(f);
            free(s);
            return aino_from_buf(base, buf, pos);
        }
    }
    free(s);
    fclose(f);
    return 0;
}

int fsdb_used_as_nname(a_inode* base, const TCHAR* nname)
{
    FILE* f;
    byte buf[1 + 4 + 257 + 257 + 81];

    f = get_fsdb(base, L"r+b");
    if (f == 0)
    {
        if (g_curr_conf.filesys_custom_uaefsdb && (base->volflags & MYVOLUMEINFO_STREAMS))
            return custom_fsdb_used_as_nname(base, nname);
        return 0;
    }
    for (;;)
    {
        TCHAR* s;
        if (fread(buf, 1, sizeof buf, f) < sizeof buf)
            break;
        if (buf[0] == 0)
            continue;
        s = Unicode::au((char*)buf + 5 + 257);
        if (_tcscmp(s, nname) == 0)
        {
            free(s);
            fclose(f);
            return 1;
        }
        free(s);
    }
    fclose(f);
    return 0;
}

static int needs_dbentry(a_inode* aino)
{
    const TCHAR* nn_begin;

    if (aino->deleted)
        return 0;

    if (!fsdb_mode_representable_p(aino, aino->amigaos_mode) || aino->comment != 0)
        return 1;

    nn_begin = nname_begin(aino->nname);
    return _tcscmp(nn_begin, aino->aname) != 0;
}

static void write_aino(FILE* f, a_inode* aino)
{
    byte buf[1 + 4 + 257 + 257 + 81] = { 0 };

    buf[0] = aino->needs_dbentry;
    do_put_mem_long((uint*)(buf + 1), aino->amigaos_mode);
    Unicode::ua_copy((char*)buf + 5, 256, aino->aname);
    buf[5 + 256] = '\0';
    Unicode::ua_copy((char*)buf + 5 + 257, 256, nname_begin(aino->nname));
    buf[5 + 257 + 256] = '\0';
    Unicode::ua_copy((char*)buf + 5 + 2 * 257, 80, aino->comment ? aino->comment : L"");
    buf[5 + 2 * 257 + 80] = '\0';
    aino->db_offset = ftell(f);
    fwrite(buf, 1, sizeof buf, f);
    aino->has_dbentry = aino->needs_dbentry;
    TRACE_FSDB((L"%d '%s' '%s' written\n", aino->db_offset, aino->aname, aino->nname));
}

/* Write back the db file for a directory.  */

void fsdb_dir_writeback(a_inode* dir)
{
    FILE* f;
    int changes_needed = 0;
    int entries_needed = 0;
    a_inode* aino;
    byte* tmpbuf;
    int size, i;

    TRACE_FSDB((L"fsdb writeback %s\n", dir->aname));
    /* First pass: clear dirty bits where unnecessary, and see if any work
     * needs to be done.  */
    for (aino = dir->child; aino; aino = aino->sibling)
    {
        /*
           int old_needs_dbentry = aino->needs_dbentry || aino->has_dbentry;
           aino->needs_dbentry = needs_dbentry (aino);
           entries_needed |= aino->has_dbentry | aino->needs_dbentry;
         */
        int old_needs_dbentry = aino->has_dbentry;
        int need = needs_dbentry(aino);
        aino->needs_dbentry = need;
        entries_needed |= need;
        if (!aino->dirty)
            continue;
        if (!aino->needs_dbentry && !old_needs_dbentry)
            aino->dirty = 0;
        else
            changes_needed = 1;
    }
    if (!entries_needed)
    {
        kill_fsdb(dir);
        TRACE_FSDB((L"fsdb removed\n"));
        return;
    }

    if (!changes_needed)
    {
        TRACE_FSDB((L"not modified\n"));
        return;
    }

    f = get_fsdb(dir, L"r+b");
    if (f == 0)
    {
        if ((g_curr_conf.filesys_custom_uaefsdb && (dir->volflags & MYVOLUMEINFO_STREAMS)) || g_curr_conf.filesys_no_uaefsdb)
        {
            for (aino = dir->child; aino; aino = aino->sibling)
            {
                aino->dirty = 0;
                aino->has_dbentry = 0;
                aino->needs_dbentry = 0;
            }
            return;
        }
        f = get_fsdb(dir, L"w+b");
        if (f == 0)
        {
            TRACE_FSDB((L"failed\n"));
            /* This shouldn't happen... */
            return;
        }
    }
    fseek(f, 0, SEEK_END);
    size = ftell(f);
    fseek(f, 0, SEEK_SET);
    tmpbuf = 0;
    if (size > 0)
    {
        tmpbuf = (byte*)malloc(size);
        fread(tmpbuf, 1, size, f);
    }
    TRACE_FSDB((L"**** updating '%s' %d\n", dir->aname, size));

    for (aino = dir->child; aino; aino = aino->sibling)
    {
        if (!aino->dirty)
            continue;
        aino->dirty = 0;

        i = 0;
        while (!aino->has_dbentry && i < size)
        {
            TCHAR* s = Unicode::au((char*)tmpbuf + i + 5);
            if (!_tcscmp(s, aino->aname))
            {
                aino->has_dbentry = 1;
                aino->db_offset = i;
            }
            free(s);
            i += 1 + 4 + 257 + 257 + 81;
        }

        if (!aino->has_dbentry)
        {
            fseek(f, 0, SEEK_END);
            aino->has_dbentry = 1;
        }
        else
        {
            fseek(f, aino->db_offset, SEEK_SET);
        }
        write_aino(f, aino);
    }
    TRACE_FSDB((L"end\n"));
    fclose(f);
    free(tmpbuf);
}